#include <fstream>
#include <vector>
#include <cstdlib>

#include <jtl/result.hpp>
#include <jtl/string_builder.hpp>

#include <jank/error/aot.hpp>
#include <jank/error/system.hpp>
#include <jank/aot/processor.hpp>
#include <jank/runtime/context.hpp>
#include <jank/runtime/module/loader.hpp>
#include <jank/util/cli.hpp>
#include <jank/util/fmt.hpp>
#include <jank/util/fmt/print.hpp>
#include <jank/util/scope_exit.hpp>
#include <jank/util/clang.hpp>
#include <jank/util/environment.hpp>

namespace jank::aot
{
  using namespace jank::runtime;

  static jtl::immutable_string relative_to_cache_dir(jtl::immutable_string const &file_path)
  {
    return util::format("{}/{}", __rt_ctx->binary_cache_dir, file_path);
  }

  // TODO: Generate an object file instead of a cpp
  static jtl::immutable_string gen_entrypoint(jtl::immutable_string const &module)
  {
    jtl::string_builder sb;
    sb(R"(/* DO NOT MODIFY: Autogenerated by jank. */

using jank_object_ref = void*;
using jank_bool = char;
using jank_usize = unsigned long long;

extern "C" int jank_init_with_pch(int const argc,
                         char const ** const argv,
                         jank_bool const init_default_ctx,
                         char const * const pch_data,
                         jank_usize pch_size,
                         int (*fn)(int const, char const ** const));
extern "C" jank_object_ref jank_load_clojure_core_native();
extern "C" jank_object_ref jank_load_clojure_core();
extern "C" jank_object_ref jank_load_jank_compiler_native();
extern "C" jank_object_ref jank_var_intern_c(char const *, char const *);
extern "C" jank_object_ref jank_deref(jank_object_ref);
extern "C" jank_object_ref jank_call2(jank_object_ref, jank_object_ref, jank_object_ref);
extern "C" void jank_module_set_loaded(char const *module);
extern "C" jank_object_ref jank_parse_command_line_args(int, char const **);
)");

    auto const modules_rlocked{ __rt_ctx->loaded_modules_in_order.rlock() };
    for(auto const &it : *modules_rlocked)
    {
      util::format_to(sb,
                      R"(extern "C" jank_object_ref {}();)",
                      module::module_to_load_function(it));
      sb("\n");
    }

    /* TODO: Embed all registered resources. */
    auto const pch_path{ util::find_pch(util::binary_version()) };
    sb(util::format(R"(
namespace
{
  char const incremental_pch[]
  {
    #embed "{}"
  };
}
        )",
                    pch_path.unwrap()));

    sb(R"(

int main(int argc, const char** argv)
{
  auto const fn{ [](int const argc, char const **argv) {
    jank_load_clojure_core_native();
    jank_load_clojure_core();
    jank_module_set_loaded("/clojure.core");
    jank_load_jank_compiler_native();

    )");

    for(auto const &it : *modules_rlocked)
    {
      util::format_to(sb, "{}();\n", module::module_to_load_function(it));
    }

    sb(R"(auto const apply{ jank_var_intern_c("clojure.core", "apply") };)");
    sb("\n");
    sb(R"(auto const command_line_args{ jank_parse_command_line_args(argc, argv) };)");
    sb("\n");

    util::format_to(sb, R"(auto const fn(jank_var_intern_c("{}", "-main"));)", module);
    sb("\n");
    sb(R"(jank_call2(jank_deref(apply), jank_deref(fn), command_line_args);

    return 0;

  } };

  return jank_init_with_pch(argc, argv, true, incremental_pch, sizeof(incremental_pch), fn);
}
  )");

    auto const tmp_dir{ std::filesystem::temp_directory_path() };
    std::string main_file_path{ tmp_dir / "jank-main-XXXXXX" };

    auto const fd{ mkstemp(main_file_path.data()) };
    close(fd);

    std::ofstream out(main_file_path);
    out << sb.release();

    return main_file_path;
  }

  jtl::result<void, error_ref> processor::compile(jtl::immutable_string const &module) const
  {
    auto const main_var(__rt_ctx->find_var(module, "-main"));
    if(main_var.is_nil())
    {
      return error::aot_unresolved_main(util::format(
        "The entrypoint of the program is expected to be #'{}/-main, but this var is missing.",
        module));
    }

    std::vector<char const *> compiler_args{};

    auto const modules_rlocked{ __rt_ctx->loaded_modules_in_order.rlock() };
    for(auto const &it : *modules_rlocked)
    {
      /* Core modules will be linked as part of libjank-standalone.a. */
      if(runtime::module::is_core_module(it))
      {
        continue;
      }

      auto const &module_path{ util::format("{}.o",
                                            relative_to_cache_dir(module::module_to_path(it))) };

      if(std::filesystem::exists(module_path.c_str()))
      {
        compiler_args.push_back(strdup(module_path.c_str()));
      }
      else
      {
        auto const find_res{ __rt_ctx->module_loader.find(it, module::origin::latest) };
        if(find_res.is_ok() && find_res.expect_ok().sources.o.is_some())
        {
          compiler_args.push_back(strdup(find_res.expect_ok().sources.o.unwrap().path.c_str()));
        }
        else
        {
          return error::internal_aot_failure(util::format("Compiled module '{}' not found.", it));
        }
      }
    }

    auto const entrypoint_path{ gen_entrypoint(module) };
    compiler_args.push_back(strdup("-x"));
    compiler_args.push_back(strdup("c++"));
    compiler_args.push_back(strdup(entrypoint_path.c_str()));

    for(auto const &include_dir : util::cli::opts.include_dirs)
    {
      compiler_args.push_back(strdup(util::format("-I{}", include_dir).c_str()));
    }

    auto const clang_path_str{ util::find_clang() };
    if(clang_path_str.is_none())
    {
      return error::system_failure(
        util::format("Unable to find Clang {}.", JANK_CLANG_MAJOR_VERSION));
    }
    auto const clang_dir{ std::filesystem::path{ clang_path_str.unwrap().c_str() }.parent_path() };
    compiler_args.emplace_back(strdup("-I"));
    compiler_args.emplace_back(strdup((clang_dir / "../include").c_str()));
    compiler_args.emplace_back(
      strdup(util::format("-Wl,-rpath,{}", (clang_dir / "../lib")).c_str()));

    std::filesystem::path const jank_path{ util::process_dir().c_str() };
    compiler_args.emplace_back(strdup("-L"));
    compiler_args.emplace_back(strdup(jank_path.c_str()));

    std::filesystem::path const jank_resource_dir{ util::resource_dir().c_str() };
    compiler_args.emplace_back(strdup("-I"));
    compiler_args.emplace_back(strdup(util::format("{}/include", jank_resource_dir).c_str()));
    compiler_args.emplace_back(strdup("-L"));
    compiler_args.emplace_back(strdup(util::format("{}/lib", jank_resource_dir).c_str()));

    {
      std::string_view const flags{ JANK_AOT_FLAGS };
      size_t start{};
      while(start < flags.size())
      {
        auto end{ flags.find(' ', start) };
        if(end == std::string_view::npos)
        {
          end = flags.size();
        }

        auto const token{ flags.substr(start, end - start) };
        if(!token.empty())
        {
          compiler_args.push_back(strdup(std::string{ token }.c_str()));
        }

        start = end + 1;
      }
    }

    if constexpr(jtl::current_platform == jtl::platform::macos_like)
    {
      compiler_args.push_back(strdup("-L/opt/homebrew/lib"));
    }

    for(auto const &library_dir : util::cli::opts.library_dirs)
    {
      compiler_args.push_back(strdup(util::format("-L{}", library_dir).c_str()));
    }

    for(auto const &lib : { "-ljank-standalone",
                            /* Default libraries that jank depends on. */
                            "-lm",
                            "-lLLVM",
                            "-lclang-cpp",
                            "-lcrypto",
                            "-lz",
                            "-lzstd" })
    {
      compiler_args.push_back(strdup(lib));
    }

    /* On non-macOS platforms, explicitly link libstdc++.
     * macOS uses libc++ implicitly via Clang. */
    if constexpr(jtl::current_platform != jtl::platform::macos_like)
    {
      compiler_args.push_back(strdup("-lstdc++"));
    }

    for(auto const &define : util::cli::opts.define_macros)
    {
      compiler_args.push_back(strdup(util::format("-D{}", define).c_str()));
    }

    compiler_args.push_back(strdup("-std=c++20"));
    compiler_args.push_back(strdup("-Wno-c23-extensions"));
    if constexpr(jtl::current_platform == jtl::platform::linux_like)
    {
      compiler_args.push_back(strdup("-Wl,--export-dynamic"));
    }
    compiler_args.push_back(strdup("-rdynamic"));
    compiler_args.push_back(strdup("-O2"));

    /* Required because of `strdup` usage and need to manually free the memory.
     * Clang expects C strings that we own. */
    /* TODO: I doubt this is really needed. These strings aren't captured by Clang. */
    util::scope_exit const cleanup{ [&]() {
      for(auto const s : compiler_args)
      {
        /* NOLINTNEXTLINE(cppcoreguidelines-no-malloc) */
        free(reinterpret_cast<void *>(const_cast<char *>(s)));
      }
    } };

    compiler_args.push_back(strdup("-o"));
    compiler_args.push_back(strdup(util::cli::opts.output_filename.c_str()));

    //util::println("compilation command: {} ", compiler_args);

    auto const res{ util::invoke_clang(compiler_args) };
    if(res.is_err())
    {
      return res.expect_err();
    }

    return ok();
  }
}
